﻿using System;
using System.Windows;
using Bettzueche.RLLibrary.Types;
using Bettzueche.RLLibrary.Component;

namespace AMEE1
{
    /// <summary>
    /// Umwelt (künstliche).
    /// </summary>
    /// <remarks>
    /// 
    /// </remarks>
    /// <example>
    /// using Bettzueche.RLLibrary.Functions; // BumpinessFunction, Hilfsfunktionen
    /// namespace NSExample {
    ///     class Example 
    ///     {
    ///         static void DoExample() {
    ///             int anzSektoren = 4;
    ///             float[] spurRadien = { 0.5f, 1.0f }; // zwei Spuren
    ///             MapConfiguration weltConfig = new MapConfiguration(anzSektoren, spurRadien);
    ///             BumpinessFunction realWelt = HilfsFunktionen.Weltfunktion2;
    /// 
    ///             SimuSensor sensor = new SimuSensor(weltConfig, realWelt);
    ///             Vergeltungssunktion rewardFunc = Vergeltungssunktion.StandardVergeltung;
    /// 
    ///             Environment env = new Environment(sensor, rewardFunc);
    /// 
    ///             State state = env.State; // aktueller Zustand
    ///             Console.WriteLine("Aktueller Zustand:\n" + state.ToString());
    ///             
    ///             // Aktion "Gehe um 0.75 Richtung X-Achse und -0.75 Richtung Y-Achse"
    ///             Action act = new Action { X = 0.75, Y = -0.75 };
    ///             double reward = env.DoAction(act, out state); // gibt reward und next-state
    ///             Console.WriteLine("Neuer Zustand:\n " + state.ToString());
    ///             Console.WriteLine("Bewertung der letzten Aktion: {0:F3}", reward);
    ///         }
    ///     }
    /// }
    /// 
    /// </example>
    class Environment : IEnvironment<State, Action, double>
    {
        
        #region Fields

        /// <summary>Belohnung bei Erreichen der Zielkoordinate. Initial / standard = 1.000.</summary>
        internal double ZielBelohnung = 1000;

        /// <summary>Minimale Abstand zum Ziel, der als 'Treffer' gewertet wird. (typischerw. Hälfte erste Spurbreite)</summary>
        private double _minimumAbstandZumZiel;

        /// <summary>Position in der 'realen' Welt</summary>
        private Vector _realKoordinate;

        /// <summary>Sensorschnittstelle zum Messen der Unebenheiten.</summary>
        SimuSensor _sensor;

        #endregion


        #region IEnvironment Properties

        /// <summary>
        /// Gets the state.
        /// </summary>
        /// <value>
        /// The state.
        /// </value>
        /// <exception cref="System.NotImplementedException"></exception>
        public State State {
            get { return _zustand; } //return _zustand.Clone()
        }
        State _zustand;
        

        public bool IsFinal {
            get {
                Vector abstand = this._realKoordinate - Ziel;
                return Math.Abs(abstand.Length) < _minimumAbstandZumZiel;
            }
        }

        #endregion


        #region Poperties

        /// <summary>
        /// Gets or sets the Zielkoordinate, bei der es ein Belohnung gibt.
        /// </summary>
        /// <value>
        /// The ziel.
        /// </value>
        public Vector Ziel { get; set; }

        /// <summary>
        /// Gibt die Schnittstelle / Funktion zur Bewertung einer Aktion.
        /// </summary>
        /// <value>
        /// Die <see cref="Vergeltungsfunktion"/>.
        /// </value>
        public Vergeltungssunktion Vergeltung { get; protected set; }

        /// <summary>
        /// Gets the (readonly) sensor.
        /// </summary>
        public SimuSensor Sensor { get { return _sensor; } }

        /// <summary>
        /// Gets or sets the dim combi.
        /// </summary>
        /// <value>
        /// The dim combi.
        /// </value>
        public DimensionCombination DimensionCombi { get; set; }

        /// <summary>
        /// Gets or sets the bounding bumpiness. Default: 1.<para>
        /// Maximale Unebenheit, die noch betreten werden kann. Wird versucht, eine Koordinate mit höheren
        /// u-Wert zu betreten, wird die Aktion nicht ausgeführt und es gibt eine hohe Bestrafung.</para>
        /// </summary>
        /// <value>
        /// The bumpiness bound. Should 0.0 - 1.0.
        /// </value>
        public double BumpinessBound { get; set; }

        /// <summary>
        /// Gets or sets the bound penalty. Default: 0.<para>
        /// Strafe (reward), wenn versucht wird, eine Koordinate mit Unebenheit größer als
        /// <c>BumoinessBound</c> zu betreten.</para>
        /// </summary>
        /// <value>
        /// The bound penalty.
        /// </value>
        public double BoundPenalty { get; set; }

        #endregion


        #region Constructor(s)

        /// <summary>
        /// Initializes a new instance of the <see cref="Environment" /> class
        /// with start position = (0, 0).
        /// </summary>
        /// <param name="sensor">The sensor.</param>
        /// <param name="feedback">The feedback.</param>
        /// <exception cref="System.ArgumentNullException">
        /// sensor
        /// or
        /// feedback
        /// </exception>
        /// <exception cref="System.ArgumentException">
        /// inconsistent data;feedback
        /// or
        /// Eval-Delegate is null;feedback
        /// </exception>
        /// <remarks>
        /// Setzt Standardwert für MinimumAbstandZumZiel auf Hälfte der ersten (innersten) Spurbreite.
        /// Initialisiert den aktuellen 
        /// <see cref="Environment#State">Zustand</see>.
        /// </remarks>
        public Environment(SimuSensor sensor, Vergeltungssunktion feedback) 
            : this(sensor, feedback, 0.0, 0.0, new DimensionCombination(sensor.MapConf)) {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Environment" /> class.
        /// </summary>
        /// <param name="sensor">The sensor.</param>
        /// <param name="feedback">The feedback.</param>
        /// <param name="startX">The start x.</param>
        /// <param name="startY">The start y.</param>
        /// <param name="combi"></param>
        /// <exception cref="System.ArgumentNullException">
        /// sensor
        /// or
        /// feedback
        /// or
        /// combi
        /// </exception>
        /// <exception cref="System.ArgumentException">
        /// inconsistent data;sensor
        /// </exception>
        /// <remarks>
        /// Setzt Standardwert für MinimumAbstandZumZiel auf Hälfte der ersten (innersten) Spurbreite.
        /// Initialisiert den aktuellen 
        /// <see cref="Environment#State">Zustand</see>.
        /// </remarks>
        public Environment(SimuSensor sensor, Vergeltungssunktion feedback, double startX, double startY, DimensionCombination combi) {
            if (sensor == null) {
                throw new ArgumentNullException("sensor");
            }
            if (sensor.MapConf == null || sensor.MapConf.SpurRadien == null || sensor.MapConf.SpurRadien.Length == 0) {
                throw new ArgumentException("inconsistent data", "sensor");
            }
            if (feedback == null) {
                throw new ArgumentNullException("feedback");
            }
            if (combi == null) {
                throw new ArgumentNullException("combi");
            }
            
            _sensor = sensor;
            Vergeltung = feedback;
            DimensionCombi = combi;
            _realKoordinate = new Vector { X = startX, Y = startY }; // Startposition
            // Ziel = Standard (0 , 0)
            _minimumAbstandZumZiel = (float)_sensor.MapConf.SpurRadien.Last();
            Init(null);
            // Begrenzung der Welt:
            BumpinessBound = 1.0; // u >= 1 ist nicht betretbar
        }

        #endregion


        #region IEnvironment Members

        /// <summary>
        /// Inits the environment.
        /// </summary>
        /// <param name="specifications">is ignored</param>
        public void Init(string specifications) {
            DoAction(new Action(), out _zustand); // Initialisiert aktuellen Zustand
        }

        /// <summary>
        /// Does the action. (Geerbt von IEnvironment)
        /// </summary>
        /// <param name="action">The action of Type <see cref="Action"/>.</param>
        /// <param name="nextState">next State.</param>
        /// <returns>
        /// Reward for performing the given action.
        /// </returns>
        /// <exception cref="System.NotSupportedException">wenn action nicht vom Typ <see cref="Action"/> ist.</exception>
        public double DoAction(IAction action, out IState nextState) {
            if (action is Action) {
                State stat;// = nextState as WorldImage;
                double resu = this.DoAction((Action)action, out stat);
                nextState = stat;
                return resu;
            }
            else {
                throw new NotSupportedException(action.GetType() + " is not supported for action.");
            }
        }

        #endregion


        #region IEnvironment<S,A> Member

        /// <summary>
        /// Angegebene Aktion ausführen.
        /// </summary>
        /// <param name="action">auszuführende Aktion</param>
        /// <param name="nextState">Folgezustand (wird gesetzt; siehe auch Prop State)</param>
        /// <returns>Belohnung r (Reward)</returns>
        public double DoAction(Action a, out State nextState) {
            Action action = this.FillAction(a);
            Vector altePos = _realKoordinate;
            if (IsBounding(action)) {
                nextState = _zustand; // besser clone()?
                return BoundPenalty;
            }

            // Setze aktuelle Realweltpostion auf RealX + a.X und RealY + a.Y 
            // (Real-Koord nur für Simulation der Realwelt, die Agentenwelt ist die in State definierte Radialkarte)
            _realKoordinate.X += action.X;
            _realKoordinate.Y += action.Y;

            // Ermittle (Messe) des WeltAbbild, setze neue Zielrichtung und gib das als Folgezustand
            double[] unebenheiten = _sensor.Messe(_realKoordinate.X, _realKoordinate.Y);
            Vector vAgentZiel = this.Ziel - _realKoordinate;
            _zustand = new State(vAgentZiel, unebenheiten, DimensionCombi);
            nextState = this.State;

            // Ermittlung der Vergeltung:
            double alteZielEntfernung = (Ziel - altePos).Length;
            double neueZielEntfernung = (Ziel - _realKoordinate).Length;
            double zielnaherung = neueZielEntfernung - alteZielEntfernung;
            double unebenheit = _sensor.MessePunkt(_realKoordinate.X, _realKoordinate.Y); // Unebenheit am Ziel
            // Bestrafung / Belohnung (Vergeltung):
            double r = 0.0;
            if (this.IsFinal) {
                r = ZielBelohnung;
            }
            else
                r = Vergeltung(unebenheit, zielnaherung);
            return r;
        }

        #endregion


        #region Methods

        /// <summary>
        /// Den Agenten auf angegebene Position setzten.<para>
        /// Typischerweise wird diese Methode nur zu Beginn einer neuen Lern-Episode aufgerufen.
        /// Initialisiert diese Umwelt neu.
        /// </para>
        /// </summary>
        /// <param name="startX">The start x.</param>
        /// <param name="startY">The start y.</param>
        internal void ReSpawn(double startX, double startY) {
            _realKoordinate = new Vector { X = startX, Y = startY }; // Startposition
            Init(null);
        }

        /// <summary>
        /// Prüft, ob die angegeben Aktion auf zu große Unebenheit führt.
        /// </summary>
        /// <param name="action">The action.</param>
        /// <returns></returns>
        bool IsBounding(Action action) {
            Vector neuePos =  new Vector { X = _realKoordinate.X + action.X, Y = _realKoordinate.Y + action.Y };
            double uZiel = _sensor.MessePunkt(neuePos.X, neuePos.Y);
            return (uZiel > BumpinessBound);
        }

        /// <summary>
        /// Erzeugt eine voll ausgefüllte Action zur action, wenn diese nur eine ID hat.
        /// Workaround für die generisch erzeugten Actions in EpsilonGreedy.
        /// </summary>
        /// <param name="action">The action.</param>
        /// <returns></returns>
        Action FillAction(Action action) {
            // Workaround für die generisch erzeugten Actions in EpsilonGreedy<A>:
            if (action.X == 0.0 && action.Y == 0.0) {
                Action retVal = _sensor.MapConf.GetAction(action.ID);
                return retVal;
            }
            else
                return action;
        }

        #endregion



    }
}
